package com.googlecode.cswish.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.struts.ContextHolder;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.util.Functions;
import com.googlecode.cswish.util.TagUtil;

/**
 * Store page context information in the whole site lifecycle
 * 
 * TODO: use request scope? how to resolve 2 phases parse
 * 	(the inject operation occurs only when tag runs)?
 * 
 * @author Jerry
 *
 */
public class FreemarkerPageContext {
	
	public final static int PRI_SYSTEM = 0;
	public final static int PRI_FRAMEWORK = 10;
	public final static int PRI_BIZ = 20;

	private boolean topPage;
	
	private Set<String> cssCategories = new HashSet<>();
	
	private Map<String, PriContent> headerScript;
	private Map<String, PriContent> headerHtml;
	
	private Map<String, PriContent> footerScript;
	private Map<String, PriContent> footerHtml;
	
	// process flag
	private boolean processed;

	// cache
	private String headerCache;
	private String footerCache;
	
	private class PriContent {
		private int priority;
		private String content;
		
		public PriContent(int priority, String content) {
			this.priority = priority;
			this.content = content;
		}
		
		@Override
		public String toString() {
			return this.content;
		}

		public boolean isSame(int priority, String trimContent) {
			return this.priority == priority && this.content.equals(trimContent);
		}
	}
	
	private PriContent EMPTY = new PriContent(10, "");
	
	public boolean isTopPage() {
		return topPage;
	}

	public void setTopPage(boolean topPage) {
		this.topPage = topPage;
	}

	public FreemarkerPageContext() {
	}
	
	public boolean enableInject(String name, String type, String point, boolean once) {
		boolean enable = true;
		if (once && name != null && name.length() > 0) {
			Map<String, PriContent> map = getMap(type, point, false);
			enable = map == null || !map.containsKey(name);
		}
		return enable;
	}
	
	public void deject(String name, String type, String point) {
		Map<String, PriContent> map = getMap(type, point, true);
		if (map.put(name, EMPTY) != null) {
			headerCache = null;
			footerCache = null;
		}
	}
	
	public void inject(String name, String type, String point, boolean once, int priority, String content) {
		Map<String, PriContent> map = getMap(type, point, true);
		String key = (name == null || name.length() == 0) ? content : name;
		String trimContent = content.trim();
		PriContent existed = map.put(key, new PriContent(priority, trimContent));
		if (existed == null || !existed.isSame(priority, trimContent)) {
			headerCache = null;
			footerCache = null;
		}
	}
	
	private Map<String, PriContent> getMap(String type, String point, boolean autoCreation) {
		Map<String, PriContent> map = null;
		
		if ("header".equals(point)) {
			if ("script".equals(type)) {
				if (autoCreation && headerScript == null) {
					headerScript = new LinkedHashMap<>();
				}
				map = headerScript;
			} else if ("html".equals(type)) {
				if (autoCreation && headerHtml == null) {
					headerHtml = new LinkedHashMap<>();
				}
				map = headerHtml;
			}
		} else if ("footer".equals(point)){
			if ("script".equals(type)) {
				if (autoCreation && footerScript == null) {
					footerScript = new LinkedHashMap<>();
				}
				map = footerScript;
			} else if ("html".equals(type)) {
				if (autoCreation && footerHtml == null) {
					footerHtml = new LinkedHashMap<>();
				}
				map = footerHtml;
			}
		}
		return map;
	}
	
	public String getHeaderScript() {
		return getSortContent(headerScript);
	}
	
	public boolean hasHeaderScript() {
		return headerScript != null && headerScript.size() > 0;
	}
	
	public String getHeaderHtml() {
		return getSortContent(headerHtml);
	}
	
	public boolean hasHeaderHtml() {
		return headerHtml != null && headerHtml.size() > 0;
	}
	
	public String getFooterScript() {
		return getSortContent(footerScript);
	}
	
	public boolean hasFooterScript() {
		return footerScript != null && footerScript.size() > 0;
	}
	
	public String getFooterHtml() {
		return getSortContent(footerHtml);
	}
	
	public boolean hasFooterHtml() {
		return footerHtml != null && footerHtml.size() > 0;
	}
	
	public void freeze() {
		buildHeader();
		buildFooter();
	}
	
	public String buildHeader() {
		processed = true;
		
		if (headerCache == null) {			
			StringBuilder sb = new StringBuilder();
			if (isTopPage()) {
				sb.append("<script>\n");
				sb.append("g_base = '").append(Functions.getBase()).append("';\n");
				
				if (Config.getInstance().isEnableSitePath()) {
					String site = ContextHolder.get().getSiteName();
					sb.append("g_site = '/").append(site == null ? "" : site).append("';\n");
				} else {
					sb.append("g_site = '';\n");
				}
				
				sb.append("g_base_site = g_base + g_site;\n");
				sb.append("g_scriptCache = [];\n");
				sb.append("</script>\n");
				
				TagUtil tagUtil = ObjectFactory.getInstance().getBean(TagUtil.class, false);
				// jquery-1.7.1
				String jqueryPath = tagUtil.getScriptPath("/static/espirit/scripts/jquery-1.7.1.js", false);
				sb.append("<script src='").append(jqueryPath).append("' type='text/javascript'></script>\n");
				// core
				String corePath = tagUtil.getScriptPath("/static/espirit/scripts/core.js", false);
				sb.append("<script src='").append(corePath).append("' type='text/javascript'></script>\n");
			}
			
			if (hasHeaderScript()) {
				String str = getHeaderScript();
				if (str.length() > 0) {
					sb.append("<script>").append(str).append("</script>");
				}
			}
			if (hasHeaderHtml()) {
				sb.append(getHeaderHtml());
			}
			headerCache = sb.toString();
		}
		return headerCache;
	}
	
	public String buildFooter() {
		processed = true;
		
		if (footerCache == null) {			
			StringBuilder sb = new StringBuilder();
			if (hasFooterScript()) {
				String str = getFooterScript();
				if (str.length() > 0) {
					sb.append("<script>").append(str).append("</script>");
				}
			}
			if (hasFooterHtml()) {
				sb.append(getFooterHtml());
			}
			footerCache = sb.toString();
		}
		return footerCache;
	}
	
	private String getSortContent(Map<String, PriContent> contents) {
		if (contents == null || contents.size() == 0) {
			return "";
		}
		
		List<PriContent> values = new ArrayList<>(contents.values());
		Collections.sort(values, new Comparator<PriContent>() {
			@Override
			public int compare(PriContent o1, PriContent o2) {
				return o1.priority - o2.priority;
			}
		});
		StringBuilder sb = new StringBuilder();
		for (PriContent content : values) {
			if (sb.length() > 0) {
				sb.append('\n');
			}
			sb.append(content.content);
		}
		return sb.toString();
	}
	
	/**
	 * SiteMeshFilter.wrapper calls this method to ensure that the header and footer only output once
	 * 
	 * @return
	 */
	public boolean isProcessed() {
		return processed;
	}

	public void setProcessed(boolean processed) {
		this.processed = processed;
	}

	public void beginRequest() {
		processed = false;				// sitemesh filter checks this flag
	}
	
	/**
	 * merge PageContext, don't override the existed value
	 * 
	 * @param another
	 * @return
	 */
	public FreemarkerPageContext merge(FreemarkerPageContext another, boolean isTopPage) {
		FreemarkerPageContext freemarkerPageContext = new FreemarkerPageContext();
		freemarkerPageContext.setTopPage(isTopPage);
		
		// content
		freemarkerPageContext.headerHtml = mergeMap(this.headerHtml, another.headerHtml);
		freemarkerPageContext.headerScript = mergeMap(this.headerScript, another.headerScript);
		freemarkerPageContext.footerHtml = mergeMap(this.footerHtml, another.footerHtml);
		freemarkerPageContext.footerScript = mergeMap(this.footerScript, another.footerScript);
		
		freemarkerPageContext.freeze();
		return freemarkerPageContext;
	}
	
	// map1 has the higher priority than map2
	private Map<String, PriContent> mergeMap(Map<String, PriContent> map1, Map<String, PriContent> map2) {
		if (map1 == null && map2 == null) {
			return null;
		}
		
		Map<String, FreemarkerPageContext.PriContent> result = new HashMap<>();
		if (map1 != null) {
			result.putAll(map1);
		}
		if (map2 != null) {
			for (Entry<String, PriContent> entry : map2.entrySet()) {
				if (!result.containsKey(entry.getKey())) {
					result.put(entry.getKey(), entry.getValue());
				}
			}
		}
		return result;
	}

	public void addCssCategory(String category) {
		this.cssCategories.add(category);
	}
	
	public Collection<String> getCssCategories() {
		return cssCategories;
	}
}